/* ========================================================================
 * ZUI: Chart.js
 * http://chartjs.org/
 * ========================================================================
 * Copyright 2013 Nick Downie
 * Released under the MIT license
 * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
 * ======================================================================== */

(function()
{
    //Define the global Chart Variable as a class.
    window.Chart = function(context)
    {

        var chart = this;


        //Easing functions adapted from Robert Penner's easing equations
        //http://www.robertpenner.com/easing/

        var animationOptions = {
            linear: function(t)
            {
                return t;
            },
            easeInQuad: function(t)
            {
                return t * t;
            },
            easeOutQuad: function(t)
            {
                return -1 * t * (t - 2);
            },
            easeInOutQuad: function(t)
            {
                if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
                return -1 / 2 * ((--t) * (t - 2) - 1);
            },
            easeInCubic: function(t)
            {
                return t * t * t;
            },
            easeOutCubic: function(t)
            {
                return 1 * ((t = t / 1 - 1) * t * t + 1);
            },
            easeInOutCubic: function(t)
            {
                if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
                return 1 / 2 * ((t -= 2) * t * t + 2);
            },
            easeInQuart: function(t)
            {
                return t * t * t * t;
            },
            easeOutQuart: function(t)
            {
                return -1 * ((t = t / 1 - 1) * t * t * t - 1);
            },
            easeInOutQuart: function(t)
            {
                if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t;
                return -1 / 2 * ((t -= 2) * t * t * t - 2);
            },
            easeInQuint: function(t)
            {
                return 1 * (t /= 1) * t * t * t * t;
            },
            easeOutQuint: function(t)
            {
                return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
            },
            easeInOutQuint: function(t)
            {
                if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t;
                return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
            },
            easeInSine: function(t)
            {
                return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
            },
            easeOutSine: function(t)
            {
                return 1 * Math.sin(t / 1 * (Math.PI / 2));
            },
            easeInOutSine: function(t)
            {
                return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
            },
            easeInExpo: function(t)
            {
                return (t == 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
            },
            easeOutExpo: function(t)
            {
                return (t == 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
            },
            easeInOutExpo: function(t)
            {
                if (t == 0) return 0;
                if (t == 1) return 1;
                if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1));
                return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
            },
            easeInCirc: function(t)
            {
                if (t >= 1) return t;
                return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
            },
            easeOutCirc: function(t)
            {
                return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
            },
            easeInOutCirc: function(t)
            {
                if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
                return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
            },
            easeInElastic: function(t)
            {
                var s = 1.70158;
                var p = 0;
                var a = 1;
                if (t == 0) return 0;
                if ((t /= 1) == 1) return 1;
                if (!p) p = 1 * .3;
                if (a < Math.abs(1))
                {
                    a = 1;
                    var s = p / 4;
                }
                else var s = p / (2 * Math.PI) * Math.asin(1 / a);
                return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
            },
            easeOutElastic: function(t)
            {
                var s = 1.70158;
                var p = 0;
                var a = 1;
                if (t == 0) return 0;
                if ((t /= 1) == 1) return 1;
                if (!p) p = 1 * .3;
                if (a < Math.abs(1))
                {
                    a = 1;
                    var s = p / 4;
                }
                else var s = p / (2 * Math.PI) * Math.asin(1 / a);
                return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
            },
            easeInOutElastic: function(t)
            {
                var s = 1.70158;
                var p = 0;
                var a = 1;
                if (t == 0) return 0;
                if ((t /= 1 / 2) == 2) return 1;
                if (!p) p = 1 * (.3 * 1.5);
                if (a < Math.abs(1))
                {
                    a = 1;
                    var s = p / 4;
                }
                else var s = p / (2 * Math.PI) * Math.asin(1 / a);
                if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
                return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * .5 + 1;
            },
            easeInBack: function(t)
            {
                var s = 1.70158;
                return 1 * (t /= 1) * t * ((s + 1) * t - s);
            },
            easeOutBack: function(t)
            {
                var s = 1.70158;
                return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
            },
            easeInOutBack: function(t)
            {
                var s = 1.70158;
                if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
                return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
            },
            easeInBounce: function(t)
            {
                return 1 - animationOptions.easeOutBounce(1 - t);
            },
            easeOutBounce: function(t)
            {
                if ((t /= 1) < (1 / 2.75))
                {
                    return 1 * (7.5625 * t * t);
                }
                else if (t < (2 / 2.75))
                {
                    return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + .75);
                }
                else if (t < (2.5 / 2.75))
                {
                    return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375);
                }
                else
                {
                    return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375);
                }
            },
            easeInOutBounce: function(t)
            {
                if (t < 1 / 2) return animationOptions.easeInBounce(t * 2) * .5;
                return animationOptions.easeOutBounce(t * 2 - 1) * .5 + 1 * .5;
            }
        };

        //Variables global to the chart
        var width = context.canvas.width;
        var height = context.canvas.height;


        //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
        if (window.devicePixelRatio)
        {
            context.canvas.style.width = width + "px";
            context.canvas.style.height = height + "px";
            context.canvas.height = height * window.devicePixelRatio;
            context.canvas.width = width * window.devicePixelRatio;
            context.scale(window.devicePixelRatio, window.devicePixelRatio);
        }

        this.Line = function(data, options)
        {

            chart.Line.defaults = {
                scaleOverlay: false,
                scaleOverride: false,
                scaleSteps: null,
                scaleStepWidth: null,
                scaleStartValue: null,
                scaleLineColor: "rgba(0,0,0,.1)",
                scaleLineWidth: 1,
                scaleShowLabels: false,
                scaleLabel: "<%=value%>",
                scaleFontFamily: "'Arial'",
                scaleFontSize: 12,
                scaleFontStyle: "normal",
                scaleFontColor: "#666",
                scaleShowGridLines: false,
                scaleGridLineColor: "rgba(0,0,0,.05)",
                scaleGridLineWidth: 1,
                bezierCurve: true,
                pointDot: false,
                pointDotRadius: 4,
                pointDotStrokeWidth: 2,
                datasetStroke: true,
                datasetStrokeWidth: 1,
                datasetFill: true,
                animation: true,
                animationSteps: 30,
                animationEasing: "easeOutQuart",
                onAnimationComplete: null
            };
            var config = (options) ? mergeChartConfig(chart.Line.defaults, options) : chart.Line.defaults;

            return new Line(data, config, context);
        }

        var clear = function(c)
        {
            c.clearRect(0, 0, width, height);
        };

        var Line = function(data, config, ctx)
        {
            var maxSize, scaleHop, calculatedScale, labelHeight, scaleHeight, valueBounds, labelTemplateString, valueHop, widestXLabel, xAxisLength, yAxisPosX, xAxisPosY, rotateLabels = 0;

            calculateDrawingSizes();

            valueBounds = getValueBounds();
            //Check and set the scale
            labelTemplateString = (config.scaleShowLabels) ? config.scaleLabel : "";
            if (!config.scaleOverride)
            {

                calculatedScale = calculateScale(scaleHeight, valueBounds.maxSteps, valueBounds.minSteps, valueBounds.maxValue, valueBounds.minValue, labelTemplateString);
            }
            else
            {
                calculatedScale = {
                    steps: config.scaleSteps,
                    stepValue: config.scaleStepWidth,
                    graphMin: config.scaleStartValue,
                    labels: []
                }
                populateLabels(labelTemplateString, calculatedScale.labels, calculatedScale.steps, config.scaleStartValue, config.scaleStepWidth);
            }

            scaleHop = Math.floor(scaleHeight / calculatedScale.steps);
            calculateXAxisSize();
            animationLoop(config, drawScale, drawLines, ctx);

            function drawLines(animPc)
            {
                for (var i = 0; i < data.datasets.length; i++)
                {
                    ctx.strokeStyle = data.datasets[i].strokeColor;
                    ctx.lineWidth = config.datasetStrokeWidth;
                    ctx.beginPath();
                    ctx.moveTo(yAxisPosX, xAxisPosY - animPc * (calculateOffset(data.datasets[i].data[0], calculatedScale, scaleHop)))

                    for (var j = 1; j < data.datasets[i].data.length; j++)
                    {
                        if (config.bezierCurve)
                        {
                            ctx.bezierCurveTo(xPos(j - 0.5), yPos(i, j - 1), xPos(j - 0.5), yPos(i, j), xPos(j), yPos(i, j));
                        }
                        else
                        {
                            ctx.lineTo(xPos(j), yPos(i, j));
                        }
                    }
                    ctx.stroke();
                    if (config.datasetFill)
                    {
                        ctx.lineTo(yAxisPosX + (valueHop * (data.datasets[i].data.length - 1)), xAxisPosY);
                        ctx.lineTo(yAxisPosX, xAxisPosY);
                        ctx.closePath();
                        ctx.fillStyle = data.datasets[i].fillColor;
                        ctx.fill();
                    }
                    else
                    {
                        ctx.closePath();
                    }
                    if (config.pointDot)
                    {
                        ctx.fillStyle = data.datasets[i].pointColor;
                        ctx.strokeStyle = data.datasets[i].pointStrokeColor;
                        ctx.lineWidth = config.pointDotStrokeWidth;
                        for (var k = 0; k < data.datasets[i].data.length; k++)
                        {
                            ctx.beginPath();
                            ctx.arc(yAxisPosX + (valueHop * k), xAxisPosY - animPc * (calculateOffset(data.datasets[i].data[k], calculatedScale, scaleHop)), config.pointDotRadius, 0, Math.PI * 2, true);
                            ctx.fill();
                            ctx.stroke();
                        }
                    }
                }

                function yPos(dataSet, iteration)
                {
                    return xAxisPosY - animPc * (calculateOffset(data.datasets[dataSet].data[iteration], calculatedScale, scaleHop));
                }

                function xPos(iteration)
                {
                    return yAxisPosX + (valueHop * iteration);
                }
            }

            function drawScale()
            {
                //X axis line
                ctx.lineWidth = config.scaleLineWidth;
                ctx.strokeStyle = config.scaleLineColor;
                ctx.beginPath();
                ctx.moveTo(width - widestXLabel / 2 + 5, xAxisPosY);
                ctx.lineTo(width - (widestXLabel / 2) - xAxisLength - 5, xAxisPosY);
                ctx.stroke();


                if (rotateLabels > 0)
                {
                    ctx.save();
                    ctx.textAlign = "right";
                }
                else
                {
                    ctx.textAlign = "center";
                }
                ctx.fillStyle = config.scaleFontColor;
                for (var i = 0; i < data.labels.length; i++)
                {
                    ctx.save();
                    // if (rotateLabels > 0){
                    //   ctx.translate(yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize);
                    //   ctx.rotate(-(rotateLabels * (Math.PI/180)));
                    //   ctx.fillText(data.labels[i], 0,0);
                    //   ctx.restore();
                    // }

                    // else{
                    //   ctx.fillText(data.labels[i], yAxisPosX + i*valueHop,xAxisPosY + config.scaleFontSize+3);
                    // }

                    ctx.beginPath();
                    ctx.moveTo(yAxisPosX + i * valueHop, xAxisPosY + 3);

                    //Check i isnt 0, so we dont go over the Y axis twice.
                    if (config.scaleShowGridLines && i > 0)
                    {
                        ctx.lineWidth = config.scaleGridLineWidth;
                        ctx.strokeStyle = config.scaleGridLineColor;
                        ctx.lineTo(yAxisPosX + i * valueHop, 5);
                    }
                    else
                    {
                        ctx.lineTo(yAxisPosX + i * valueHop, xAxisPosY + 3);
                    }
                    ctx.stroke();
                }

                //Y axis
                ctx.lineWidth = config.scaleLineWidth;
                ctx.strokeStyle = config.scaleLineColor;
                // ctx.beginPath();
                // ctx.moveTo(yAxisPosX,xAxisPosY+5);
                // ctx.lineTo(yAxisPosX,5);
                // ctx.stroke();

                ctx.textAlign = "right";
                ctx.textBaseline = "middle";
                for (var j = 0; j < calculatedScale.steps; j++)
                {
                    ctx.beginPath();
                    ctx.moveTo(yAxisPosX - 3, xAxisPosY - ((j + 1) * scaleHop));
                    if (config.scaleShowGridLines)
                    {
                        ctx.lineWidth = config.scaleGridLineWidth;
                        ctx.strokeStyle = config.scaleGridLineColor;
                        ctx.lineTo(yAxisPosX + xAxisLength + 5, xAxisPosY - ((j + 1) * scaleHop));
                    }
                    else
                    {
                        ctx.lineTo(yAxisPosX - 0.5, xAxisPosY - ((j + 1) * scaleHop));
                    }

                    ctx.stroke();

                    if (config.scaleShowLabels)
                    {
                        ctx.fillText(calculatedScale.labels[j], yAxisPosX - 8, xAxisPosY - ((j + 1) * scaleHop));
                    }
                }


            }

            function calculateXAxisSize()
            {
                xAxisLength = width;
                valueHop = xAxisLength / (data.labels.length - 1);
                yAxisPosX = 0;
                xAxisPosY = height;
            }

            function calculateDrawingSizes()
            {
                maxSize = height;

                //Need to check the X axis first - measure the length of each text metric, and figure out if we need to rotate by 45 degrees.
                ctx.font = config.scaleFontStyle + " " + config.scaleFontSize + "px " + config.scaleFontFamily;
                widestXLabel = 1;
                for (var i = 0; i < data.labels.length; i++)
                {
                    // var textLength = ctx.measureText(data.labels[i]).width;
                    var textLength = 0;
                    //If the text length is longer - make that equal to longest text!
                    widestXLabel = (textLength > widestXLabel) ? textLength : widestXLabel;
                }
                if (width / data.labels.length < widestXLabel)
                {
                    rotateLabels = 45;
                    if (width / data.labels.length < Math.cos(rotateLabels) * widestXLabel)
                    {
                        rotateLabels = 90;
                        maxSize -= widestXLabel;
                    }
                    else
                    {
                        maxSize -= Math.sin(rotateLabels) * widestXLabel;
                    }
                }
                else
                {
                    maxSize -= config.scaleFontSize;
                }

                labelHeight = config.scaleFontSize;

                maxSize = height;
                scaleHeight = maxSize; /////

            }

            function getValueBounds()
            {
                var upperValue = Number.MIN_VALUE;
                var lowerValue = Number.MAX_VALUE;
                for (var i = 0; i < data.datasets.length; i++)
                {
                    for (var j = 0; j < data.datasets[i].data.length; j++)
                    {
                        if (data.datasets[i].data[j] > upperValue)
                        {
                            upperValue = data.datasets[i].data[j]
                        };
                        if (data.datasets[i].data[j] < lowerValue)
                        {
                            lowerValue = data.datasets[i].data[j]
                        };
                    }
                };

                var maxSteps = Math.floor((scaleHeight / (labelHeight * 0.66)));
                var minSteps = Math.floor((scaleHeight / labelHeight * 0.5));

                return {
                    maxValue: upperValue,
                    minValue: lowerValue,
                    maxSteps: maxSteps,
                    minSteps: minSteps
                }
            }
        }

        function calculateOffset(val, calculatedScale, scaleHop)
        {
            var outerValue = calculatedScale.steps * calculatedScale.stepValue;
            var adjustedValue = val - calculatedScale.graphMin;
            var scalingFactor = CapValue(adjustedValue / outerValue, 1, 0);
            return (scaleHop * calculatedScale.steps) * scalingFactor;
        }

        function animationLoop(config, drawScale, drawData, ctx)
        {
            var animFrameAmount = (config.animation) ? 1 / CapValue(config.animationSteps, Number.MAX_VALUE, 1) : 1,
                easingFunction = animationOptions[config.animationEasing],
                percentAnimComplete = (config.animation) ? 0 : 1;



            if (typeof drawScale !== "function") drawScale = function() {};

            requestAnimFrame(animLoop);

            function animateFrame()
            {
                var easeAdjustedAnimationPercent = (config.animation) ? CapValue(easingFunction(percentAnimComplete), null, 0) : 1;
                clear(ctx);
                if (config.scaleOverlay)
                {
                    drawData(easeAdjustedAnimationPercent);
                    drawScale();
                }
                else
                {
                    drawScale();
                    drawData(easeAdjustedAnimationPercent);
                }
            }

            function animLoop()
            {
                //We need to check if the animation is incomplete (less than 1), or complete (1).
                percentAnimComplete += animFrameAmount;
                animateFrame();
                //Stop the loop continuing forever
                if (percentAnimComplete <= 1)
                {
                    requestAnimFrame(animLoop);
                }
                else
                {
                    if (typeof config.onAnimationComplete == "function") config.onAnimationComplete();
                }

            }

        }

        //Declare global functions to be called within this namespace here.


        // shim layer with setTimeout fallback
        var requestAnimFrame = (function()
        {
            return window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.oRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                function(callback)
                {
                    window.setTimeout(callback, 1000 / 60);
                };
        })();

        function calculateScale(drawingHeight, maxSteps, minSteps, maxValue, minValue, labelTemplateString)
        {
            var graphMin, graphMax, graphRange, stepValue, numberOfSteps, valueRange, rangeOrderOfMagnitude, decimalNum;

            valueRange = maxValue - minValue;

            rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange);

            graphMin = Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude);

            graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude);

            graphRange = graphMax - graphMin;

            stepValue = Math.pow(10, rangeOrderOfMagnitude);

            numberOfSteps = Math.round(graphRange / stepValue);

            //Compare number of steps to the max and min for that size graph, and add in half steps if need be.
            while (numberOfSteps < minSteps || numberOfSteps > maxSteps)
            {
                if (numberOfSteps < minSteps)
                {
                    stepValue /= 2;
                    numberOfSteps = Math.round(graphRange / stepValue);
                }
                else
                {
                    stepValue *= 2;
                    numberOfSteps = Math.round(graphRange / stepValue);
                }
            };

            var labels = [];
            populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue);

            return {
                steps: numberOfSteps,
                stepValue: stepValue,
                graphMin: graphMin,
                labels: labels

            }

            function calculateOrderOfMagnitude(val)
            {
                return Math.floor(Math.log(val) / Math.LN10);
            }


        }

        //Populate an array of all the labels by interpolating the string.
        function populateLabels(labelTemplateString, labels, numberOfSteps, graphMin, stepValue)
        {
            if (labelTemplateString)
            {
                //Fix floating point errors by setting to fixed the on the same decimal as the stepValue.
                for (var i = 1; i < numberOfSteps + 1; i++)
                {
                    labels.push(tmpl(labelTemplateString,
                    {
                        value: (graphMin + (stepValue * i)).toFixed(getDecimalPlaces(stepValue))
                    }));
                }
            }
        }

        //Max value from array
        function Max(array)
        {
            return Math.max.apply(Math, array);
        };
        //Min value from array
        function Min(array)
        {
            return Math.min.apply(Math, array);
        };
        //Default if undefined
        function Default(userDeclared, valueIfFalse)
        {
            if (!userDeclared)
            {
                return valueIfFalse;
            }
            else
            {
                return userDeclared;
            }
        };
        //Is a number function
        function isNumber(n)
        {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }
        //Apply cap a value at a high or low number
        function CapValue(valueToCap, maxValue, minValue)
        {
            if (isNumber(maxValue))
            {
                if (valueToCap > maxValue)
                {
                    return maxValue;
                }
            }
            if (isNumber(minValue))
            {
                if (valueToCap < minValue)
                {
                    return minValue;
                }
            }
            return valueToCap;
        }

        function getDecimalPlaces(num)
        {
            var numberOfDecimalPlaces;
            if (num % 1 != 0)
            {
                return num.toString().split(".")[1].length
            }
            else
            {
                return 0;
            }

        }

        function mergeChartConfig(defaults, userDefined)
        {
            var returnObj = {};
            for (var attrname in defaults)
            {
                returnObj[attrname] = defaults[attrname];
            }
            for (var attrname in userDefined)
            {
                returnObj[attrname] = userDefined[attrname];
            }
            return returnObj;
        }

        //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
        var cache = {};

        function tmpl(str, data)
        {
            // Figure out if we're getting a template, or if we need to
            // load the template - and be sure to cache the result.
            var fn = !/\W/.test(str) ?
                cache[str] = cache[str] ||
                tmpl(document.getElementById(str).innerHTML) :

                // Generate a reusable function that will serve as a template
                // generator (and which will be cached).
                new Function("obj",
                    "var p=[],print=function(){p.push.apply(p,arguments);};" +

                    // Introduce the data as local variables using with(){}
                    "with(obj){p.push('" +

                    // Convert the template into pure JavaScript
                    str
                    .replace(/[\r\t\n]/g, " ")
                    .split("<%").join("\t")
                    .replace(/((^|%>)[^\t]*)'/g, "$1\r")
                    .replace(/\t=(.*?)%>/g, "',$1,'")
                    .split("\t").join("');")
                    .split("%>").join("p.push('")
                    .split("\r").join("\\'") + "');}return p.join('');");

            // Provide some basic currying to the user
            return data ? fn(data) : fn;
        };
    }
}());
